UNPKG

datacops-cms

Version:

A modern, extensible CMS built with Next.js and Prisma.

220 lines (200 loc) 8.38 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { PrismaClient } from "@prisma/client"; import fs from "fs"; import { getToken } from "next-auth/jwt"; import { NextRequest, NextResponse } from "next/server"; import path from "path"; const prisma = new PrismaClient(); const PERMISSIONS_DIR = path.resolve(process.cwd(), "content"); const PERMISSIONS_PATH = path.join(PERMISSIONS_DIR, "api-permissions.json"); // === Helper: Check permissions === export async function checkAllowed(type: string, method: string, request: NextRequest) { const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); if (token) { if (token.role === "SUPERADMIN" || token.role === "ADMIN") return true; } if (!fs.existsSync(PERMISSIONS_PATH)) return true; const perms = JSON.parse(fs.readFileSync(PERMISSIONS_PATH, "utf-8")); return perms?.[type]?.[method] !== false; } // === Helper: Capitalize first letter for Prisma model === function modelName(type: string) { return type.charAt(0).toUpperCase() + type.slice(1); } // === Helper: Get relation fields for all content-types === function getAllRelationFieldsFromContentTypes(): Record<string, string[]> { const contentTypesFolder = path.resolve(process.cwd(), "content-types"); const result: Record<string, string[]> = {}; if (!fs.existsSync(contentTypesFolder)) return result; const files = fs.readdirSync(contentTypesFolder).filter(f => f.endsWith(".json")); for (const file of files) { try { const schema = JSON.parse(fs.readFileSync(path.join(contentTypesFolder, file), "utf-8")); if (!schema.name || !Array.isArray(schema.fields)) continue; const modelName = schema.name; const relationFields = schema.fields .filter((f: any) => f.type === "relation") .map((f: any) => f.name); result[modelName] = relationFields; } catch { /* ignore errors */ } } return result; } // === GET: List all items for type, with optional relations === export async function GET( req: NextRequest, { params }: { params: { type: string } } ) { const { type } = await params; const allowed = await checkAllowed(type, "GET", req); if (!allowed) { return NextResponse.json({ error: "You are not allowed to perform this action" }, { status: 403 }); } try { const model = modelName(type); const now = new Date(); const searchParams = new URL(req.url).searchParams; const populateParam = searchParams.get("populate"); // e.g. "*", "projectTeam,author" // --- Build dynamic "include" for Prisma --- let include: Record<string, boolean> | undefined; if (populateParam) { const allRelationFields = getAllRelationFieldsFromContentTypes(); const modelRelationFields = allRelationFields[type] || []; include = {}; if (populateParam === "*") { modelRelationFields.forEach(field => { include![field] = true; }); } else { for (const rel of populateParam.split(",").map(s => s.trim()).filter(Boolean)) { if (modelRelationFields.includes(rel)) include[rel] = true; } } if (Object.keys(include).length === 0) include = undefined; } // Auth const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET }); let items; if (token) { // All data for logged-in user // @ts-ignore items = await prisma[model].findMany({ ...(include ? { include } : {}) }); } else { // Published or scheduled-and-due only // @ts-ignore items = await prisma[model].findMany({ where: { OR: [ { status: "Published" }, { status: "Scheduled", schedule: { lte: now } } ] }, ...(include ? { include } : {}) }); } // Promote scheduled-to-due to published in DB and response interface ScheduledItem { id: number | string; status: string; schedule?: string | Date | null; [key: string]: any; } const scheduledToPublish: ScheduledItem[] = items.filter( (item: ScheduledItem) => item.status === "Scheduled" && item.schedule && new Date(item.schedule) <= now ); if (scheduledToPublish.length) { await Promise.all( scheduledToPublish.map(item => // @ts-ignore prisma[model].update({ where: { id: item.id }, data: { status: "Published", schedule: null } }) ) ); items = items.map((item: ScheduledItem): ScheduledItem => item.status === "Scheduled" && item.schedule && new Date(item.schedule) <= now ? { ...item, status: "Published", schedule: null } : item ); } return NextResponse.json(items); } catch (e: any) { return NextResponse.json( { error: `Error fetching data for type "${params.type}": ${e.message}` }, { status: 404 } ); } } // === POST: Create a new item for type === export async function POST( req: NextRequest, { params }: { params: { type: string } } ) { const { type } = await params; const allowed = await checkAllowed(type, "POST", req); if (!allowed) { return NextResponse.json({ error: "You are not allowed to perform this action" }, { status: 403 }); } if (!type) { return NextResponse.json( { error: "Type parameter is required" }, { status: 400 } ); } try { const model = modelName(type); const data = await req.json(); // Manage status/schedule logic if (!data.status) data.status = "Draft"; if (data.status === "Published") { data.schedule = null; } if (data.status === "Scheduled") { if (!data.schedule) { return NextResponse.json( { error: "Schedule date is required for Scheduled status." }, { status: 400 } ); } const scheduleDate = new Date(data.schedule); if (isNaN(scheduleDate.getTime()) || scheduleDate <= new Date()) { return NextResponse.json( { error: "Schedule date must be a valid future date/time." }, { status: 400 } ); } data.schedule = scheduleDate.toISOString(); } else { data.schedule = null; } // === Relations (many-to-many, etc): Convert value to { connect: ... } if needed === // If you use the same trick in your frontend, you can skip this block. // Otherwise, parse relation fields and wrap in { connect: [...] } const allRelationFields = getAllRelationFieldsFromContentTypes(); const modelRelationFields = allRelationFields[type] || []; for (const rel of modelRelationFields) { // If relation field is array of IDs, convert to { connect: [{id}] } if (Array.isArray(data[rel])) { data[rel] = { connect: data[rel].map((id: string) => ({ id })) }; } } // @ts-ignore const created = await prisma[model].create({ data }); return NextResponse.json(created, { status: 201 }); } catch (e: any) { console.error("Error creating data:", e); return NextResponse.json( { error: `Error creating data for type "${type}": ${e.message}` }, { status: 400 } ); } }